package tongxunlu.uuid;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.channels.FileLock;
import java.util.Enumeration;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;


public class IlrstcUuid {

    static private FileLock fileLock = null;
    static private File lockFile = null;
    static private AtomicInteger atomInt;
    static private Random randomizer; // to create random part
    static private int _ip; // to buffer the ip part
    static private int _lId; // to buffer the variant part

    public IlrstcUuid(
            int ip, int lId, int random, int serial, long time, int clock) {
        this.ip = ip;
        this.lId = lId;
        this.random = random;
        this.serial = serial;
        this.time = time;
        this.clock = clock;
    }

    static {
        refresh();
    }
    final private int ip;
    final private int lId;
    final private int random;
    final private int serial;
    final private long time;
    final private int clock;
    private String shortString; // to save the generated short string
    private String seperatedString; // save the seperated string

    /*
     * find the ip address, initialize the randomizer and variant part.
     */
    static public void refresh() {
        _ip = findIp();
        long nano = System.nanoTime();
        randomizer = new Random(nano);
        _lId = findLocalId(randomizer);
        atomInt = new AtomicInteger();
    }

    /*
     * return the ip address of the running machine, or loopback address is
     * returned if other can not be found, or 0 returned if an error occured.
     */
    static private int findIp() {
        int loopback = 0;
        try {
            Enumeration<NetworkInterface> eni =
                    NetworkInterface.getNetworkInterfaces();
            while (eni.hasMoreElements()) {
                NetworkInterface ni = eni.nextElement();
                byte addr[];
                if (ni.isLoopback()) {
                    Enumeration<InetAddress> eia = ni.getInetAddresses();
                    while (eia.hasMoreElements()) {
                        InetAddress ia = eia.nextElement();
                        if (ia instanceof Inet4Address) {
                            addr = ia.getAddress();
                            loopback = bytesToInt(addr);
                        }
                    }
                } else {
                    Enumeration<InetAddress> eia = ni.getInetAddresses();
                    while (eia.hasMoreElements()) {
                        InetAddress ia = eia.nextElement();
                        if (ia instanceof Inet4Address) {
                            addr = ia.getAddress();
                            return bytesToInt(addr);
                        }
                    }
                }
            }
            return loopback;
        } catch (SocketException e) {
            // TODO (Fyi) log the exception, network interface may not be found.
            return loopback;
        }
    }

    /*
     * try to aquire a lock using an integer, and such integer will be the local
     * id if the lock succeed.
     */
    static private int findLocalId(Random r) {
        // release previous lock if exists
        try {
            if (fileLock != null) {
                fileLock.release();
                fileLock.channel().close();
                fileLock = null;
            }
            if (lockFile != null) {
                lockFile.delete();
                lockFile = null;
            }
        } catch (IOException e) {
            Logger.getLogger(IlrstcUuid.class.getCanonicalName()).log(
                    Level.WARNING, "error in releasing file lock", e);
        }

        // directory to store the lock
        String propName = IlrstcUuid.class.getCanonicalName() + ".lockpath";
        String path = System.getProperty(propName);
        path = path == null ? System.getProperty("java.io.tmpdir") : path;
        File dir = new File(path == null ? "" : path);
        dir = dir.getAbsoluteFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }

        while (true) {
            // new id
            int id = r.nextInt(256);
            String filename = "Uuidlock." + id;
            File file = new File(dir, filename);

            Logger.getLogger(IlrstcUuid.class.getCanonicalName()).log(
                    Level.CONFIG, String.format(
                    "locking on file %s", file.getAbsolutePath()));
            try {
                // try lock
                if (!file.exists()) {
                    file.createNewFile();
                }
                FileOutputStream fileout = new FileOutputStream(file);
                FileLock lock = fileout.getChannel().tryLock();

                // lock successfully, return the id
                if (lock != null) {
                    fileLock = lock;
                    lockFile = file;
                    return id;
                }

                // lock fail, try next lock
            } catch (IOException e) {
                Logger.getLogger(IlrstcUuid.class.getCanonicalName()).log(
                        Level.WARNING, "failed in creating file lock", e);
                return id;
            }
        }
    }

    static private int bytesToInt(byte[] bs) {
        return (((bs[0] << 8 | (bs[1] & 0xff)) << 8
                | (bs[2] & 0xff)) << 8 | (bs[3] & 0xff));
    }

    /**
     * Generate a UUID.
     *
     * @return
     */
    static public IlrstcUuid generateUUID() {
        int i = _ip;
        int l = _lId;
        int r = randomizer.nextInt(0x10000);
        int s = atomInt.getAndIncrement() & 0xff;
        long t = (System.currentTimeMillis() & 0xFFFFFFFFFF0l) >> 4;
        int c = (int) (System.nanoTime() & 0xFFFFFFl);
        return new IlrstcUuid(i, l, r, s, t, c);
    }

    public int getIp() {
        return ip;
    }

    public long getTimeMillis() {
        return time;
    }

    public int getRandom() {
        return random;
    }

    public int getSerial() {
        return serial;
    }

    public int getClockSeq() {
        return clock;
    }

    public int getLocalId() {
        return lId;
    }

    /**
     * return the seperated string form.
     *
     * @return
     * @see #toSeperatedString()
     */
    @Override
    public String toString() {
        return toSeperatedString();
    }

    /**
     * Return the UUID string without "-".
     *
     * @return
     * @see #toSeperatedString()
     */
    public String toShortString() {
        if (shortString != null) {
            return shortString;
        }
        shortString = toSeperatedString().replace("-", "");
        return shortString;
    }

    /**
     * Return the UUID string, which is seperated by "-".
     *
     * @return
     * @see #toShortString()
     */
    public String toSeperatedString() {
        if (seperatedString != null) {
            return seperatedString;
        }
        String format = "%08X-%02X-%04X%02X-%010X-%06X";
        seperatedString =
                String.format(format, ip, lId, random, serial, time, clock);
        return seperatedString;
    }

    /**
     * Parse the str to a UUID instance.
     *
     * @param str
     * @return
     * @see #fromSeperatedString(java.lang.String)
     * @throws java.lang.RuntimeException If any error occurs during parsing.
     */
    static public IlrstcUuid fromShortString(String str) {
        try {
            int i = (int) Long.valueOf(str.substring(0, 8), 16).longValue();
            int l = Integer.valueOf(str.substring(8, 10), 16);
            int r = Integer.valueOf(str.substring(10, 14), 16);
            int s = Integer.valueOf(str.substring(14, 16), 16);
            long t = Long.valueOf(str.substring(16, 26), 16);
            int c = Integer.valueOf(str.substring(26), 16);
            return new IlrstcUuid(i, l, r, s, t, c);
        } catch (RuntimeException e) {
            throw new RuntimeException("The string is illegle.", e);
        }
    }

    /**
     * Parse the "-" seperated string to a UUID instance.
     *
     * @param str
     * @return
     * @see #fromShortString(java.lang.String)
     * @throws java.lang.RuntimeException If any error occurs during parsing.
     */
    static public IlrstcUuid fromSeperatedString(String str) {
        if (!str.matches("[0-9a-fA-F]{8}\\-[0-9a-fA-F]{2}\\-"
                + "[0-9a-fA-F]{6}\\-[0-9a-fA-F]{10}\\-[0-9a-fA-F]{6}")) {
            throw new RuntimeException("The string is illegle.");
        }
        return fromShortString(str.replace("-", ""));
    }

    @Override
    public int hashCode() {
        return toSeperatedString().hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final IlrstcUuid other = (IlrstcUuid) obj;
        return toSeperatedString().equals(other.toSeperatedString());
    }
}
